package com.p2p.core;

import android.content.Context;
import android.util.Log;

import com.p2p.define.Constant;
import com.p2p.define.P2PRoomType;
import com.p2p.listener.P2PRoomListener;
import com.p2p.listener.P2PSocketRoomListener;
import com.p2p.model.P2PRoom;
import com.p2p.model.P2PUser;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.DataChannel;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.VideoCapturerAndroid;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;

import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class P2PPeerClient extends P2PRoom {

    private final static String TAG = P2PPeerClient.class.getCanonicalName();

    private PeerConnectionFactory connectionFactory;
    private Map<String, Peer> peerHashMap = new ConcurrentHashMap<>();
    private LinkedList<PeerConnection.IceServer> iceServers = new LinkedList<>();
    private MediaConstraints mediaConstraints = new MediaConstraints();

    private MediaStream localMediaStream;
    private VideoCapturerAndroid videoCapturerAndroid;
    private VideoSource videoSource;
    private AudioSource audioSource;

    private P2PRoomListener p2PRoomListener;

    private P2PWebSocketClient webSocketClient;

    public P2PPeerClient(String roomId, Context context, P2PRoomListener listener) {
        this.setId(roomId);
        this.p2PRoomListener = listener;
        connectionFactory = P2PPeerConnectionFactory.getPeerConnectionFactory(context);
        final MessageHandler messageHandler = new MessageHandler();

        this.webSocketClient = P2PWebSocketClient.getInstance(roomId, new P2PSocketRoomListener() {
            @Override
            public void onMessage(JSONObject jsonObject) {
                messageHandler.onMessage(jsonObject);
            }
        });

        iceServers.add(new PeerConnection.IceServer("stun:stun.l.google.com:19302"));
        iceServers.add(new PeerConnection.IceServer(Constant.ICE_STUN_URL));
        iceServers.add(new PeerConnection.IceServer(Constant.ICE_TURN_URL, Constant.ICE_TURN_NAME, Constant.ICE_TURN_PASSWORD));

        mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
        mediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));
        mediaConstraints.optional.add(new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
    }

    /**
     * Send a message through the signaling server
     *
     * @param to      id of recipient
     * @param type    type of message
     * @param payload payload of message
     * @throws JSONException
     */
    private void sendMessage2Other(String to, String type, JSONObject payload) throws JSONException {

        JSONObject message = new JSONObject();
        message.put("to", to);
        //附带上roomId
        message.put("roomId", getId());
        message.put("type", type);
        message.put("payload", payload);
        message.put("action", "message");
        webSocketClient.sendMessage(message.toString());

    }

    @Override
    public void dataChannelTalk(String message) {
        Log.e(TAG, "DataChannel dataChannelTalk-" + message);
        super.dataChannelTalk(message);
        byte[] msg = message.getBytes();
        DataChannel.Buffer buffer = new DataChannel.Buffer(ByteBuffer.wrap(msg), false);
        for (Map.Entry<String, Peer> entry : peerHashMap.entrySet()) {
            entry.getValue().dataChannel.send(buffer);
        }
    }

    private class CreateOfferCommand implements P2PCommand {
        public void execute(String peerId, JSONObject payload) {
            Peer peer = peerHashMap.get(peerId);
            peer.connection.createOffer(peer, mediaConstraints);
        }
    }

    private class CreateAnswerCommand implements P2PCommand {
        public void execute(String peerId, JSONObject payload) throws JSONException {
            Peer peer = peerHashMap.get(peerId);
            SessionDescription sdp = new SessionDescription(
                    SessionDescription.Type.fromCanonicalForm(payload.getString("type")),
                    payload.getString("sdp")
            );
            peer.connection.setRemoteDescription(peer, sdp);
            peer.connection.createAnswer(peer, mediaConstraints);
        }
    }

    private class SetRemoteSDPCommand implements P2PCommand {
        public void execute(String peerId, JSONObject payload) throws JSONException {
            Peer peer = peerHashMap.get(peerId);
            SessionDescription sdp = new SessionDescription(
                    SessionDescription.Type.fromCanonicalForm(payload.getString("type")),
                    payload.getString("sdp")
            );
            peer.connection.setRemoteDescription(peer, sdp);
        }
    }

    private class AddIceCandidateCommand implements P2PCommand {
        public void execute(String peerId, JSONObject payload) throws JSONException {
            PeerConnection pc = peerHashMap.get(peerId).connection;
            if (pc.getRemoteDescription() != null) {
                IceCandidate candidate = new IceCandidate(
                        payload.getString("id"),
                        payload.getInt("label"),
                        payload.getString("candidate")
                );
                pc.addIceCandidate(candidate);
            }
        }
    }

    private class MessageHandler {

        private HashMap<String, P2PCommand> commandMap;

        private MessageHandler() {
            this.commandMap = new HashMap<>();
            commandMap.put("subscribe", new CreateOfferCommand());
            commandMap.put("offer", new CreateAnswerCommand());
            commandMap.put("answer", new SetRemoteSDPCommand());
            commandMap.put("candidate", new AddIceCandidateCommand());
        }

        private void onMessage(JSONObject data) {
            try {
                Log.e(TAG, data.toString());
                String action = data.getString("action");
                switch (action) {
                    case "init":
                        break;
                    case "join":
                        //
                        JSONObject roomObj = data.getJSONObject("room");
                        P2PPeerClient.this.setName(roomObj.getString("name"));
                        P2PPeerClient.this.setType(roomObj.getInt("roomType"));

                        JSONArray userJsonArr = roomObj.getJSONArray("user");
                        for (int index = 0; index < userJsonArr.length(); index++) {
                            JSONObject userJson = userJsonArr.getJSONObject(index);
                            String uid = userJson.optString("uid");
                            JSONObject subJson = new JSONObject();
                            subJson.put("roomId", getId());
                            sendMessage2Other(uid, "subscribe", null);
                        }
                        p2PRoomListener.onJoinSuccess(P2PPeerClient.this);
                        break;
                    case "message":
                        String from = data.getString("from");
                        String type = data.getString("type");
                        JSONObject payload = null;
                        if (!type.equals("subscribe")) {
                            payload = data.getJSONObject("payload");
                        }
                        if (!peerHashMap.containsKey(from)) {
                            String name = data.optString("name");
                            addPeer(from, name);
                            commandMap.get(type).execute(from, payload);
                        } else {
                            commandMap.get(type).execute(from, payload);
                        }
                        break;
                    case "disconnect":
                        String uid = data.optString("uid");
                        removePeer(uid);
                        break;
                    default:
                        Log.e(TAG, data.toString());
                        break;
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 添加Peer
     *
     * @param id
     * @return
     */
    private Peer addPeer(String id, String name) {
        Peer peer = new Peer(id, name);
        peerHashMap.put(id, peer);
        return peer;
    }

    /**
     * Call this method in Activity.onPause()
     */
    @Override
    public void onPause() {
        super.onPause();
        if (videoSource != null) videoSource.stop();
    }

    /**
     * Call this method in Activity.onResume()
     */
    @Override
    public void onResume() {
        super.onResume();
        if (videoSource != null) videoSource.restart();
    }

    /**
     * Call this method in Activity.onDestroy()
     */
    @Override
    public void onDestroy() {
        webSocketClient.leaveRoom(this.getId(), this.getType());
        for (Peer peer : peerHashMap.values()) {
            if (peer.dataChannel != null) {
                peer.dataChannel.close();
                peer.dataChannel.dispose();
            }
            if (peer.connection != null) {
                if (localMediaStream != null) {
                    peer.connection.removeStream(localMediaStream);
                }
                peer.connection.close();
                peer.connection.dispose();
            }
            peerHashMap.remove(peer.getId());
        }
        if (localMediaStream != null) {
            localMediaStream.dispose();
        }
        if (videoSource != null) {
            videoSource.dispose();
        }
        if (audioSource != null) {
            audioSource.dispose();
        }
    }

    private void removePeer(String id) {
        Peer peer = peerHashMap.get(id);
        p2PRoomListener.onRemoveRemoteStream(peer);
        if (peer.dataChannel != null) {
            peer.dataChannel.close();
            peer.dataChannel.dispose();
        }
        if (peer.connection != null) {
            peer.connection.removeStream(localMediaStream);
            peer.connection.close();
            peer.connection.dispose();
        }
        peerHashMap.remove(peer.getId());
        p2PRoomListener.onUserLeft(peer);
    }

    private class Peer extends P2PUser implements SdpObserver, PeerConnection.Observer {

        private PeerConnection connection;
        private DataChannel dataChannel;

        public Peer(final String id, final String name) {
            Log.e(TAG, "new Peer " + id + " " + name);
            this.connection = connectionFactory.createPeerConnection(iceServers, mediaConstraints, this);
            this.setId(id);
            this.setName(name);
            if (P2PPeerClient.this.getType() == P2PRoomType.Media) {
                if (localMediaStream == null) {
                    startWithVideoAndAudio();
                }
                connection.addStream(localMediaStream);
            }
            DataChannel.Init init = new DataChannel.Init();
            init.id = 0;
            dataChannel = connection.createDataChannel("Cool DataChannel", init);
            dataChannel.registerObserver(new DataChannel.Observer() {
                @Override
                public void onStateChange() {
                    Log.e(TAG, "DataChannel onStateChange-" + name + "-" + dataChannel.state().toString());
                }

                @Override
                public void onMessage(DataChannel.Buffer buffer) {
                    ByteBuffer data = buffer.data;
                    final byte[] bytes = new byte[data.capacity()];
                    data.get(bytes);
                    String msg = new String(bytes);
                    Log.e(TAG, "DataChannel onMessage-" + name + "-" + msg);
                    p2PRoomListener.onDataChannelTextMessage(Peer.this, msg);
                }
            });
        }

        //PeerConnection.Observer
        @Override
        public void onSignalingChange(PeerConnection.SignalingState signalingState) {
            Log.e(TAG, "onSignalingChange " + signalingState);
        }

        @Override
        public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
            Log.e(TAG, "onIceConnectionChange " + iceConnectionState);
            p2PRoomListener.onStatusChanged(Peer.this, iceConnectionState.toString());
            switch (iceConnectionState) {
                case DISCONNECTED:
                    removePeer(getId());
                    break;
                case CONNECTED:
                    p2PRoomListener.onUserJoined(Peer.this);
                    break;
                default:
                    break;
            }
        }

        @Override
        public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
            Log.e(TAG, "onIceGatheringChange " + iceGatheringState);
        }

        @Override
        public void onIceCandidate(IceCandidate candidate) {
            Log.e(TAG, "onIceCandidate " + candidate);
            try {
                JSONObject payload = new JSONObject();
                payload.put("label", candidate.sdpMLineIndex);
                payload.put("id", candidate.sdpMid);
                payload.put("candidate", candidate.sdp);
                sendMessage2Other(getId(), "candidate", payload);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onAddStream(MediaStream mediaStream) {
            Log.e(TAG, "onAddStream " + mediaStream);
            Peer.this.setRemoteStream(mediaStream);
            p2PRoomListener.onAddRemoteStream(Peer.this);
        }

        @Override
        public void onRemoveStream(MediaStream mediaStream) {
            Log.e(TAG, "onRemoveStream " + mediaStream);
            removePeer(getId());
        }

        @Override
        public void onDataChannel(DataChannel dataChannel) {
            Log.e(TAG, "onDataChannel " + dataChannel);
        }

        @Override
        public void onRenegotiationNeeded() {
            Log.e(TAG, "onRenegotiationNeeded " + Peer.this);
        }

        // SdpObserver
        @Override
        public void onCreateSuccess(SessionDescription sdp) {
            Log.e(TAG, "onCreateSuccess " + sdp);
            try {
                JSONObject payload = new JSONObject();
                payload.put("type", sdp.type.canonicalForm());
                payload.put("sdp", sdp.description);
                sendMessage2Other(getId(), sdp.type.canonicalForm(), payload);
                connection.setLocalDescription(Peer.this, sdp);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onSetSuccess() {
            Log.e(TAG, "onSetSuccess");
        }

        @Override
        public void onCreateFailure(String s) {
            Log.e(TAG, "onCreateFailure" + s);
        }

        @Override
        public void onSetFailure(String s) {
            Log.e(TAG, "onSetFailure " + s);
        }
    }


    /**
     * Start the client.
     * <p>
     * Set up the local stream and notify the signaling server.
     * Call this method after onCallReady.
     */

    private void startWithVideoAndAudio() {
        localMediaStream = connectionFactory.createLocalMediaStream("ARDAMS");
        //Video
        MediaConstraints videoConstraints = new MediaConstraints();
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxHeight", Integer.toString(1920)));
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxWidth", Integer.toString(1080)));
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("maxFrameRate", Integer.toString(30)));
        videoConstraints.mandatory.add(new MediaConstraints.KeyValuePair("minFrameRate", Integer.toString(30)));
        videoCapturerAndroid = VideoCapturerAndroid.create(VideoCapturerAndroid.getNameOfFrontFacingDevice());
        videoSource = connectionFactory.createVideoSource(videoCapturerAndroid, videoConstraints);
        VideoTrack videoTrack = connectionFactory.createVideoTrack("ARDAMSv0", videoSource);
        localMediaStream.addTrack(videoTrack);

        //Audio
        audioSource = connectionFactory.createAudioSource(new MediaConstraints());
        AudioTrack audioTrack = connectionFactory.createAudioTrack("ARDAMSa0", audioSource);
        localMediaStream.addTrack(audioTrack);

        p2PRoomListener.onLocalStream(localMediaStream);
    }
}
